#!/bin/sh
#
# american fuzzy lop++ - status check tool
# ----------------------------------------
#
# Originally written by Michal Zalewski
#
# Copyright 2015 Google Inc. All rights reserved.
# Copyright 2019-2022 AFLplusplus Project. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#   https://www.apache.org/licenses/LICENSE-2.0
#
# This tool summarizes the status of any locally-running synchronized
# instances of afl-fuzz.
#

echo "$0 status check tool for afl-fuzz by Michal Zalewski"
echo
test "$1" = "-h" -o "$1" = "-hh" && {
  echo "Usage: $0 [-s] [-d] afl_output_directory"
  echo
  echo Options:
  echo "  -s  -  skip details and output summary results only"
  echo "  -d  -  include dead fuzzer stats"
  echo
  exit 1
}

unset SUMMARY_ONLY
unset PROCESS_DEAD

while [ "$1" = "-s" -o "$1" = "-d" ]; do

  if [ "$1" = "-s" ]; then
    SUMMARY_ONLY=1
  fi

  if [ "$1" = "-d" ]; then
    PROCESS_DEAD=1
  fi
  
  shift

done

DIR="$1"

if [ "$DIR" = "" ]; then

  echo "Usage: $0 [-s] [-d] afl_output_directory" 1>&2
  echo 1>&2
  echo Options: 1>&2
  echo "  -s  -  skip details and output summary results only" 1>&2
  echo "  -d  -  include dead fuzzer stats" 1>&2
  echo 1>&2
  exit 1

fi

cd "$DIR" || exit 1

if [ -d queue ]; then

  echo "[-] Error: parameter is an individual output directory, not a sync dir." 1>&2
  exit 1

fi

RED=`tput setaf 9 1 1`
GREEN=`tput setaf 2 1 1`
BLUE=`tput setaf 4 1 1`
YELLOW=`tput setaf 11 1 1`
NC=`tput sgr0`
RESET="$NC"

CUR_TIME=`date +%s`

TMP=`mktemp -t .afl-whatsup-XXXXXXXX` || TMP=`mktemp -p /data/local/tmp .afl-whatsup-XXXXXXXX` || TMP=`mktemp -p /data/local/tmp .afl-whatsup-XXXXXXXX` || exit 1

ALIVE_CNT=0
DEAD_CNT=0

TOTAL_TIME=0
TOTAL_EXECS=0
TOTAL_EPS=0
TOTAL_CRASHES=0
TOTAL_PFAV=0
TOTAL_PENDING=0

# Time since last find / crash / hang, formatted as string
FMT_TIME="0 days 0 hours"
FMT_FIND="${RED}none seen yet${NC}"
FMT_CRASH="none seen yet"
FMT_HANG="none seen yet"

if [ "$SUMMARY_ONLY" = "" ]; then

  echo "Individual fuzzers"
  echo "=================="
  echo

fi

fmt_duration()
{
  DUR_STRING=
  if [ $1 -le 0 ]; then
    return 1
  fi

  local duration=$((CUR_TIME - $1))
  local days=$((duration / 60 / 60 / 24))
  local hours=$(((duration / 60 / 60) % 24))
  local minutes=$(((duration / 60) % 60))
  local seconds=$((duration % 60))

  if [ $duration -le 0 ]; then
    DUR_STRING="0 seconds"
  elif [ $duration -eq 1 ]; then
    DUR_STRING="1 second"
  elif [ $days -gt 0 ]; then
    DUR_STRING="$days days, $hours hours"
  elif [ $hours -gt 0 ]; then
    DUR_STRING="$hours hours, $minutes minutes"
  elif [ $minutes -gt 0 ]; then
    DUR_STRING="$minutes minutes, $seconds seconds"
  else
    DUR_STRING="$seconds seconds"
  fi
}

FIRST=true
TOTAL_WCOP=
TOTAL_LAST_FIND=0

for i in `find . -maxdepth 2 -iname fuzzer_stats | sort`; do

  sed 's/^command_line.*$/_skip:1/;s/[ ]*:[ ]*/="/;s/$/"/' "$i" >"$TMP"
  . "$TMP"

  RUN_UNIX=$run_time
  RUN_DAYS=$((RUN_UNIX / 60 / 60 / 24))
  RUN_HRS=$(((RUN_UNIX / 60 / 60) % 24))

  test -n "$cycles_wo_finds" && {
    test -z "$FIRST" && TOTAL_WCOP="${TOTAL_WCOP}/"
    TOTAL_WCOP="${TOTAL_WCOP}${cycles_wo_finds}"
    FIRST=
  }

  if [ "$SUMMARY_ONLY" = "" ]; then

    echo ">>> $afl_banner ($RUN_DAYS days, $RUN_HRS hrs) fuzzer PID: $fuzzer_pid <<<"
    echo

  fi

  if ! kill -0 "$fuzzer_pid" 2>/dev/null; then

    if [ "$SUMMARY_ONLY" = "" ]; then

      echo "  Instance is dead or running remotely, skipping."
      echo

    fi

    DEAD_CNT=$((DEAD_CNT + 1))
    last_find=0

    if [ "$PROCESS_DEAD" = "" ]; then

      continue

    fi

  fi

  ALIVE_CNT=$((ALIVE_CNT + 1))

  EXEC_SEC=0
  test -z "$RUN_UNIX" -o "$RUN_UNIX" = 0 || EXEC_SEC=$((execs_done / RUN_UNIX))
  PATH_PERC=$((cur_item * 100 / corpus_count))

  TOTAL_TIME=$((TOTAL_TIME + RUN_UNIX))
  TOTAL_EPS=$((TOTAL_EPS + EXEC_SEC))
  TOTAL_EXECS=$((TOTAL_EXECS + execs_done))
  TOTAL_CRASHES=$((TOTAL_CRASHES + saved_crashes))
  TOTAL_PENDING=$((TOTAL_PENDING + pending_total))
  TOTAL_PFAV=$((TOTAL_PFAV + pending_favs))

  if [ "$last_find" -gt "$TOTAL_LAST_FIND" ]; then
    TOTAL_LAST_FIND=$last_find
  fi

  if [ "$SUMMARY_ONLY" = "" ]; then

    # Warnings in red
    TIMEOUT_PERC=$((exec_timeout * 100 / execs_done))
    if [ $TIMEOUT_PERC -ge 10 ]; then
      echo "  ${RED}timeout_ratio $TIMEOUT_PERC%${NC}"
    fi

    if [ $EXEC_SEC -eq 0 ]; then
      echo "  ${YELLOW}no data yet, 0 execs/sec${NC}"
    elif [ $EXEC_SEC -lt 100 ]; then
      echo "  ${RED}slow execution, $EXEC_SEC execs/sec${NC}"
    fi

    fmt_duration $last_find && FMT_FIND=$DUR_STRING
    fmt_duration $last_crash && FMT_CRASH=$DUR_STRING
    fmt_duration $last_hang && FMT_HANG=$DUR_STRING
    FMT_CWOP="not available"
    test -n "$cycles_wo_finds" && {
      test "$cycles_wo_finds" = 0 && FMT_CWOP="$cycles_wo_finds"
      test "$cycles_wo_finds" -gt 10 && FMT_CWOP="${YELLOW}$cycles_wo_finds${NC}"
      test "$cycles_wo_finds" -gt 50 && FMT_CWOP="${RED}$cycles_wo_finds${NC}"
    }

    echo "  last_find       : $FMT_FIND"
    echo "  last_crash      : $FMT_CRASH"
    echo "  last_hang       : $FMT_HANG"
    echo "  cycles_wo_finds : $FMT_CWOP"

    CPU_USAGE=$(ps aux | grep $fuzzer_pid | grep -v grep | awk '{print $3}')
    MEM_USAGE=$(ps aux | grep $fuzzer_pid | grep -v grep | awk '{print $4}')

    echo "  cpu usage $CPU_USAGE%, memory usage $MEM_USAGE%"
    echo "  cycles $((cycles_done + 1)), lifetime speed $EXEC_SEC execs/sec, items $cur_item/$corpus_count (${PATH_PERC}%)"

    if [ "$saved_crashes" = "0" ]; then
      echo "  pending $pending_favs/$pending_total, coverage $bitmap_cvg, no crashes yet"
    else
      echo "  pending $pending_favs/$pending_total, coverage $bitmap_cvg, crashes saved $saved_crashes (!)"
    fi

    echo

  fi

done

# Formatting for total time, time since last find, crash, and hang
fmt_duration $((CUR_TIME - TOTAL_TIME)) && FMT_TIME=$DUR_STRING
# Formatting for total execution
FMT_EXECS="0 millions"
EXECS_MILLION=$((TOTAL_EXECS / 1000 / 1000))
EXECS_THOUSAND=$((TOTAL_EXECS / 1000 % 1000))
if [ $EXECS_MILLION -gt 9 ]; then
  FMT_EXECS="$EXECS_MILLION millions"
elif [ $EXECS_MILLION -gt 0 ]; then
  FMT_EXECS="$EXECS_MILLION millions, $EXECS_THOUSAND thousands"
else
  FMT_EXECS="$EXECS_THOUSAND thousands"
fi

rm -f "$TMP"

TOTAL_DAYS=$((TOTAL_TIME / 60 / 60 / 24))
TOTAL_HRS=$(((TOTAL_TIME / 60 / 60) % 24))

test -z "$TOTAL_WCOP" && TOTAL_WCOP="not available"
fmt_duration $TOTAL_LAST_FIND && TOTAL_LAST_FIND=$DUR_STRING

test "$TOTAL_TIME" = "0" && TOTAL_TIME=1

if [ "$PROCESS_DEAD" = "" ]; then

  TXT="excluded from stats"

else

  TXT="included in stats"
  ALIVE_CNT=$(($ALIVE_CNT - $DEAD_CNT))

fi

echo "Summary stats"
echo "============="
echo
echo "       Fuzzers alive : $ALIVE_CNT"

if [ ! "$DEAD_CNT" = "0" ]; then
  echo "      Dead or remote : $DEAD_CNT ($TXT)"
fi

echo "      Total run time : $FMT_TIME"
echo "         Total execs : $FMT_EXECS"
echo "    Cumulative speed : $TOTAL_EPS execs/sec"
if [ "$ALIVE_CNT" -gt "0" ]; then
  echo "       Average speed : $((TOTAL_EPS / ALIVE_CNT)) execs/sec"
fi
echo "       Pending items : $TOTAL_PFAV faves, $TOTAL_PENDING total"

if [ "$ALIVE_CNT" -gt "1" ]; then
  echo "  Pending per fuzzer : $((TOTAL_PFAV/ALIVE_CNT)) faves, $((TOTAL_PENDING/ALIVE_CNT)) total (on average)"
fi

echo "       Crashes saved : $TOTAL_CRASHES"
echo "Cycles without finds : $TOTAL_WCOP"
echo "  Time without finds : $TOTAL_LAST_FIND"
echo

exit 0
